home *** CD-ROM | disk | FTP | other *** search
/ PC Open 107 / PC Open 107 CD 1.bin / CD1 / INTERNET / EMAIL / pop file / setup.exe / Services / IMAP.pm
Encoding:
Perl POD Document  |  2004-11-29  |  71.4 KB  |  2,364 lines

  1. # POPFILE LOADABLE MODULE
  2. package Services::IMAP;
  3. use POPFile::Module;
  4. @ISA = ("POPFile::Module");
  5.  
  6. # ----------------------------------------------------------------------------
  7. #
  8. # IMAP.pm --- a module to use POPFile for an IMAP connection.
  9. #
  10. # Copyright (c) 2001-2004 John Graham-Cumming
  11. #
  12. #   This file is part of POPFile
  13. #
  14. #   POPFile is free software; you can redistribute it and/or modify
  15. #   it under the terms of the GNU General Public License as published by
  16. #   the Free Software Foundation; either version 2 of the License, or
  17. #   (at your option) any later version.
  18. #
  19. #   POPFile is distributed in the hope that it will be useful,
  20. #   but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  22. #   GNU General Public License for more details.
  23. #
  24. #   You should have received a copy of the GNU General Public License
  25. #   along with POPFile; if not, write to the Free Software
  26. #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  27. #
  28. #   Originally created by   Manni Heumann (mannih2001@users.sourceforge.net)
  29. #   Modified by             Sam Schinke (sschinke@users.sourceforge.net)
  30. #   Patches by              David Lang (davidlang@users.sourceforge.net)
  31. #   Moved location by       John Graham-Cumming (jgrahamc@users.sf.net)
  32. #
  33. #   The documentation for this module can be found on
  34. #   http://popfile.sf.net/cgi-bin/wiki.pl?ExperimentalModules/Imap
  35. #
  36. # ----------------------------------------------------------------------------
  37.  
  38. use IO::Socket;
  39. use Digest::MD5 qw( md5_hex );
  40. use strict;
  41. use warnings;
  42. use locale;
  43.  
  44. my $eol = "\015\012";
  45. my $cfg_separator = "-->";
  46.  
  47. #----------------------------------------------------------------------------
  48. # new
  49. #
  50. #   Class new() function
  51. #----------------------------------------------------------------------------
  52.  
  53. sub new
  54. {
  55.     my $type = shift;
  56.     my $self = POPFile::Module->new();
  57.  
  58.     bless $self, $type;
  59.  
  60.     $self->name( 'imap' );
  61.  
  62.     $self->{classifier__} = 0;
  63.  
  64.     # Here are the variables used by this module:
  65.  
  66.     # A place to store the last response that the IMAP server sent us
  67.     $self->{last_response__} = '';
  68.  
  69.     # A place to store the last command we sent to the server
  70.     $self->{last_command__} = '';
  71.  
  72.     # The tag that preceeds any command we sent, actually just a simple counter var
  73.     $self->{tag__} = 0;
  74.  
  75.     # A list of mailboxes on the server:
  76.     $self->{mailboxes__} = [];
  77.  
  78.     # The session id for the current session:
  79.     $self->{api_session__} = '';
  80.  
  81.     # A hash to hold per-folder data (watched and output flag + socket connection)
  82.     # This data structure is extremely important to the work done by this
  83.     # module, so don't mess with it!
  84.     # The hash contains one key per service folder.
  85.     # This key will return another hash. This time the keys are fixed and
  86.     # can be {output} for an output folder
  87.     # {watched} for a watched folder.
  88.     # {imap} will hold a valid socket object for the connection of this folder.
  89.     $self->{folders__} = ();
  90.  
  91.     # A flag that tells us that the folder list has changed
  92.     $self->{folder_change_flag__} = 0;
  93.  
  94.     # A hash containing the hash values of messages that we encountered
  95.     # during a single run through service().
  96.     # If you provide a hash as a key and if that key exists, the value
  97.     # will be the folder where the original message was placed (or left) in.
  98.     $self->{hash_values__} = ();
  99.  
  100.     $self->{history__} = 0;
  101.  
  102.     return $self;
  103. }
  104.  
  105.  
  106.  
  107. # ----------------------------------------------------------------------------
  108. #
  109. # initialize
  110. #
  111. # ----------------------------------------------------------------------------
  112.  
  113. sub initialize
  114. {
  115.     my ( $self ) = @_;
  116.  
  117.     $self->config_( 'hostname', '' );
  118.     $self->config_( 'port', 143 );
  119.     $self->config_( 'login', '' );
  120.     $self->config_( 'password', '' );
  121.     $self->config_( 'update_interval', 20 );
  122.     $self->config_( 'expunge', 0 );
  123.     $self->config_( 'use_ssl', 0 );
  124.  
  125.     # Those next variables have getter/setter functions and should
  126.     # not be used directly:
  127.  
  128.     $self->config_( 'watched_folders', "INBOX" );     # function watched_folders
  129.     $self->config_( 'bucket_folder_mappings', '' );   # function folder_for_bucket
  130.     $self->config_( 'uidvalidities', '' );            # function uid_validity
  131.     $self->config_( 'uidnexts', '' );                 # function uid_next
  132.  
  133.     # Diabled by default
  134.     $self->config_( 'enabled', 0 );
  135.  
  136.     # Training mode is disabled by default:
  137.     $self->config_( 'training_mode', 0 );
  138.  
  139.     # Set the time stamp for the last update to the current time
  140.     # minus the update interval so that we will connect as soon
  141.     # as service() is called for the first time.
  142.     $self->{last_update__} = time - $self->config_( 'update_interval' );
  143.  
  144.     return $self->SUPER::initialize();
  145. }
  146.  
  147.  
  148.  
  149.  
  150. # ----------------------------------------------------------------------------
  151. #
  152. # Start. Get's called by the loader and makes us run.
  153. #
  154. #   We try to connect to our IMAP server here, and get a list of
  155. #   folders / mailboxes, so we can populate the configuration UI.
  156. #
  157. # ----------------------------------------------------------------------------
  158.  
  159. sub start
  160. {
  161.     my ( $self ) = @_;
  162.  
  163.     if ( $self->config_( 'enabled' ) == 0 ) {
  164.         return 2;
  165.     }
  166.  
  167.     $self->register_configuration_item_( 'configuration',
  168.                                          'imap_0_connection_details',
  169.                                          'imap-connection-details.thtml',
  170.                                          $self );
  171.  
  172.     $self->register_configuration_item_( 'configuration',
  173.                                          'imap_1_watch_folders',
  174.                                          'imap-watch-folders.thtml',
  175.                                          $self );
  176.  
  177.     $self->register_configuration_item_( 'configuration',
  178.                                          'imap_2_watch_more_folders',
  179.                                          'imap-watch-more-folders.thtml',
  180.                                          $self );
  181.  
  182.     $self->register_configuration_item_( 'configuration',
  183.                                          'imap_3_bucket_folders',
  184.                                          'imap-bucket-folders.thtml',
  185.                                          $self );
  186.  
  187.     $self->register_configuration_item_( 'configuration',
  188.                                          'imap_4_update_mailbox_list',
  189.                                          'imap-update-mailbox-list.thtml',
  190.                                          $self );
  191.  
  192.     $self->register_configuration_item_( 'configuration',
  193.                                          'imap_5_options',
  194.                                          'imap-options.thtml',
  195.                                          $self );
  196.  
  197.     return $self->SUPER::start();
  198. }
  199.  
  200.  
  201.  
  202. # ----------------------------------------------------------------------------
  203. # stop
  204. #
  205. #   Not much to do here.
  206. #
  207. # ----------------------------------------------------------------------------
  208.  
  209. sub stop
  210. {
  211.     my ( $self ) = @_;
  212.  
  213.     if ( $self->{api_session__} ne '' ) {
  214.         $self->{classifier__}->release_session_key( $self->{api_session__} );
  215.     }
  216.  
  217.     foreach ( keys %{$self->{folders__}} ) {
  218.         if ( exists $self->{folders__}{$_}{imap} ) {
  219.             $self->{folders__}{$_}{imap}->shutdown( 2 );
  220.             delete $self->{folders__}{$_}{imap};
  221.         }
  222.     }
  223. }
  224.  
  225.  
  226.  
  227. # ----------------------------------------------------------------------------
  228. #
  229. # service
  230. #
  231. #   This get's frequently called by the framework.
  232. #   It checks whether our checking interval has elapsed and if it has,
  233. #   it goes to work.
  234. #
  235. # ----------------------------------------------------------------------------
  236.  
  237. sub service
  238. {
  239.     my ( $self ) = @_;
  240.  
  241.     if ( time - $self->{last_update__} >= $self->config_( 'update_interval' ) ) {
  242.  
  243.         # Check to see if we have obtained a session key yet
  244.         if ( $self->{api_session__} eq '' ) {
  245.             $self->{api_session__} = $self->{classifier__}->get_session_key( 'admin', '' );
  246.         }
  247.  
  248.         # Since say__() as well as get_response__() can throw an exception, i.e. die if
  249.         # they detect a lost connection, we eval the following code to be able
  250.         # to catch the exception. We also tell Perl to ignore broken pipes.
  251.  
  252.         eval {
  253.             local $SIG{'PIPE'} = 'IGNORE';
  254.             local $SIG{'__DIE__'};
  255.  
  256.             if ( $self->config_( 'training_mode' ) == 1 ) {
  257.  
  258.                 $self->train_on_archive__();
  259.  
  260.             }
  261.             else {
  262.  
  263.                 # If we haven't yet set up a list of serviced folders,
  264.                 # or if the list was changed by the user, build up a
  265.                 # list of folder in $self->{folders__}
  266.  
  267.                 if ( ( keys %{$self->{folders__}} == 0 ) || ( $self->{folder_change_flag__} == 1 ) ) {
  268.                     $self->build_folder_list__();
  269.                 }
  270.  
  271.                 # Try to establish connections, log in, and select for
  272.                 # all of our folders
  273.                 $self->connect_folders__();
  274.  
  275.                 # Now do the real job
  276.  
  277.                 foreach my $folder ( keys %{$self->{folders__}} ) {
  278.  
  279.                     if ( exists $self->{folders__}{$folder}{imap} ) {
  280.  
  281.                         $self->scan_folder( $folder );
  282.  
  283.                     }
  284.                 }
  285.  
  286.                 # Reset the hash containing the hash values we have just seen.
  287.                 $self->{hash_values__} = ();
  288.             }
  289.  
  290.         };
  291.         # if an exception occurred, we try to catch it here
  292.         if ( $@ ) {
  293.             # say__() and get_response__() will die with this message:
  294.             if ( $@ =~ /The connection to the IMAP server was lost/ ) {
  295.                 $self->log_( 0, $@ );
  296.             }
  297.             # If we didn't die but somebody else did, we have empathy.
  298.             else {
  299.                 die $@;
  300.             }
  301.         }
  302.         # Save the current time.
  303.         $self->{last_update__} = time;
  304.     }
  305.  
  306.     return 1;
  307. }
  308.  
  309.  
  310. #----------------------------------------------------------------------------
  311. # build_folder_list__
  312. #
  313. #   This function builds a list of all the folders that we have to care
  314. #   about. This list consists of the folders we are watching for new mail
  315. #   and of the folders that we are watching for reclassification requests.
  316. #   The complete list is stored in a hash: $self->{folders__}.
  317. #   The keys in this hash are the names of our folders, the values represent
  318. #   flags. Currently, the flags can be
  319. #       {watched} for watched folders and
  320. #       {output} for output/bucket folders.
  321. #   The function connect_folders__() will later add an {imap} key that will
  322. #   hold the connection for that folder.
  323. #
  324. # arguments:
  325. #   none.
  326. #
  327. # return value:
  328. #   none.
  329. #----------------------------------------------------------------------------
  330.  
  331. sub build_folder_list__
  332. {
  333.     my ( $self ) = @_;
  334.  
  335.     $self->log_( 1, "Building list of serviced folders." );
  336.  
  337.     # At this point, we simply reset the folders hash.
  338.     # This isn't really elegant because it will leave dangling connections
  339.     # if we have already been connected. But I trust in Perl's garbage collection
  340.     # and keep my fingers crossed.
  341.  
  342.     %{$self->{folders__}} = ();
  343.  
  344.     # watched folders
  345.     foreach ( $self->watched_folders__() ) {
  346.         $self->{folders__}{$_}{watched} = 1;
  347.     }
  348.  
  349.     # output folders
  350.     foreach my $bucket ( $self->{classifier__}->get_all_buckets( $self->{api_session__} ) ) {
  351.  
  352.         my $folder = $self->folder_for_bucket__( $bucket );
  353.  
  354.         if ( defined $folder ) {
  355.             $self->{folders__}{$folder}{output} = $bucket;
  356.         }
  357.     }
  358.  
  359.     # If this is a new POPFile installation that isn't yet
  360.     # configured, our hash will have exactly one key now
  361.     # which will point to the INBOX. Since this isn't enough
  362.     # to do anything meaningful, we simply reset the hash:
  363.  
  364.     if ( ( keys %{$self->{folders__}} ) == 1 ) {
  365.         %{$self->{folders__}} = ();
  366.     }
  367.  
  368.     # Reset the folder change flag
  369.     $self->{folder_change_flag__} = 0;
  370. }
  371.  
  372.  
  373.  
  374. #----------------------------------------------------------------------------
  375. # connect_folders__
  376. #
  377. #   This function will iterate over each folder found in the %{$self->{folders__}}
  378. #   hash. For each folder it will try to establish a connection, log in, and select
  379. #   the folder.
  380. #   The corresponding socket object, will be stored in
  381. #   $self->{folders__}{$folder}{imap}
  382. #
  383. # arguments:
  384. #   none.
  385. #
  386. # return value:
  387. #   none.
  388. #----------------------------------------------------------------------------
  389.  
  390. sub connect_folders__
  391. {
  392.     my ( $self ) = @_;
  393.  
  394.     # Establish a connection for each folder in the hash
  395.  
  396.     foreach my $folder ( keys %{$self->{folders__}} ) {
  397.  
  398.         # We may already have a valid connection for this folder:
  399.         if ( exists $self->{folders__}{$folder}{imap} ) {
  400.             next;
  401.         }
  402.  
  403.         $self->{folders__}{$folder}{server} = 1;
  404.         $self->{folders__}{$folder}{tag} = 0;
  405.  
  406.         # The folder may be write-only:
  407.         if ( exists $self->{folders__}{$folder}{output}
  408.                 &&
  409.             ! exists $self->{folders__}{$folder}{watched}
  410.                 &&
  411.             $self->{classifier__}->is_pseudo_bucket( $self->{api_session__},
  412.                                     $self->{folders__}{$folder}{output} ) ) {
  413.                 next;
  414.         }
  415.  
  416.         $self->log_( 1, "Trying to connect to ". $self->config_( 'hostname' ) . " for folder $folder." );
  417.         $self->{folders__}{$folder}{imap} = $self->connect( $self->config_( 'hostname' ), $self->config_( 'port' ) );
  418.  
  419.         # Did the connection succeed?
  420.         if ( defined $self->{folders__}{$folder}{imap} ) {
  421.  
  422.             if ( $self->login( $folder ) ) {
  423.  
  424.                 # Build a list of IMAP mailboxes if we haven't already got one:
  425.                 unless ( @{$self->{mailboxes__}} ) {
  426.                     $self->get_mailbox_list( $self->{folders__}{$folder}{imap} );
  427.                 }
  428.  
  429.                 # Change to / SELECT the folder
  430.                 $self->say__( $folder, "SELECT \"$folder\"" );
  431.                 if ( $self->get_response__( $folder ) != 1 ) {
  432.  
  433.                     $self->log_( 0, "Could not SELECT folder $folder." );
  434.                     $self->say__( $folder, "LOGOUT" );
  435.                     $self->get_response__( $folder );
  436.                     delete $self->{folders__}{$folder}{imap};
  437.                 }
  438.                 else {
  439.                     # And now check that our UIDs are valid
  440.                     unless ( $self->folder_uid_status__( $folder ) ) {
  441.                         $self->log_( 0, "Changed UIDVALIDITY for folder $folder. Some new messages might have been skipped." );
  442.                     }
  443.                 }
  444.             }
  445.             else {
  446.                 $self->log_( 0, "Could not LOGIN for folder $folder." );
  447.                 delete $self->{folders__}{$folder}{imap};
  448.             }
  449.         }
  450.         else {
  451.             $self->log_( 0, "Could not CONNECT for folder $folder." );
  452.             delete $self->{folders__}{$folder}{imap};
  453.         }
  454.     }
  455. }
  456.  
  457.  
  458.  
  459. # ----------------------------------------------------------------------------
  460. #
  461. # disconnect_folders__
  462. #
  463. #   The test suite needs a way to disconnect all the folders after one test is
  464. #   done and the next test needs to be done with different settings.
  465. #
  466. # ----------------------------------------------------------------------------
  467.  
  468. sub disconnect_folders__
  469. {
  470.     my ( $self ) = @_;
  471.  
  472.     foreach my $folder ( keys %{$self->{folders__}} ) {
  473.  
  474.         # We may already have a valid connection for this folder:
  475.         if ( exists $self->{folders__}{$folder}{imap} ) {
  476.             $self->logout( $folder );
  477.         }
  478.     }
  479.     %{$self->{folders__}} = ();
  480. }
  481.  
  482.  
  483. # ----------------------------------------------------------------------------
  484. #
  485. # scan_folder
  486. #
  487. #   This function scans a folder on the IMAP server.
  488. #   According to the attributes of a folder (watched, output), and the attributes
  489. #   of the message (new, classified, etc) it then decides what to do with the
  490. #   messages.
  491. #   There are currently three possible actions:
  492. #       1. Classify the message and move to output folder
  493. #       2. Reclassify message
  494. #       3. Ignore message (if you want to call that an action)
  495. #
  496. # Arguments:
  497. #
  498. #   $folder: The folder to scan.
  499. #
  500. # ----------------------------------------------------------------------------
  501.  
  502. sub scan_folder
  503. {
  504.     my ( $self, $folder) = @_;
  505.  
  506.     # make the flags more accessible.
  507.     my $is_watched = ( exists $self->{folders__}{$folder}{watched} ) ? 1 : 0;
  508.     my $is_output = ( exists $self->{folders__}{$folder}{output} ) ? $self->{folders__}{$folder}{output} : '';
  509.  
  510.     $self->log_( 1, "Looking for new messages in folder $folder." );
  511.  
  512.     # Do a NOOP first. Certain implementations won't tell us about
  513.     # new messages while we are connected and selected otherwise:
  514.  
  515.     $self->say__( $folder, "NOOP" );
  516.     my $result = $self->get_response__( $folder );
  517.     if ( $result != 1 ) {
  518.         $self->log_( 0, "NOOP failed (return value $result)" );
  519.     }
  520.  
  521.     my $moved_message = 0;
  522.     my @uids = $self->get_new_message_list( $folder );
  523.  
  524.     # We now have a list of messages with UIDs greater than or equal
  525.     # to our last stored UIDNEXT value (of course, the list might be
  526.     # empty). Let's iterate over that list.
  527.  
  528.     foreach my $msg ( @uids ) {
  529.         $self->log_( 1, "Found new message in folder $folder (UID: $msg)" );
  530.  
  531.         my $hash = $self->get_hash( $folder, $msg );
  532.  
  533.         $self->uid_next__( $folder, $msg + 1 );
  534.  
  535.         # Watch our for those pesky duplicate and triplicate spam messages:
  536.  
  537.         if ( exists $self->{hash_values__}{$hash} ) {
  538.  
  539.             my $destination = $self->{hash_values__}{$hash};
  540.             if ( $destination ne $folder ) {
  541.                 $self->log_( 0, "Found duplicate hash value: $hash. Moving the message to $destination." );
  542.                 $self->move_message( $folder, $msg, $destination );
  543.                 $moved_message++;
  544.             }
  545.             else {
  546.                 $self->log_( 0, "Found duplicate hash value: $hash. Ignoring duplicate in folder $folder." );
  547.             }
  548.  
  549.             next;
  550.         }
  551.  
  552.         # Find out what we are dealing with here:
  553.  
  554.         if ( $is_watched ) {
  555.             if ( $self->can_classify__( $hash ) ) {
  556.  
  557.                 my $result = $self->classify_message( $msg, $hash, $folder );
  558.  
  559.                 if ( defined $result ) {
  560.                     if ( $result ne '' ) {
  561.                         $moved_message++;
  562.                         $self->{hash_values__}{$hash} = $result;
  563.                     }
  564.                     else {
  565.                         $self->{hash_values__}{$hash} = $folder;
  566.                     }
  567.                 }
  568.                 next;
  569.             }
  570.         }
  571.  
  572.         if ( my $bucket = $is_output ) {
  573.             if ( my $old_bucket = $self->can_reclassify__( $hash, $bucket ) ) {
  574.  
  575.                 my $result = $self->reclassify_message( $folder, $msg, $old_bucket, $hash );
  576.  
  577.                 next;
  578.             }
  579.         }
  580.  
  581.         # If we get here despite all those next statements, we do nothing and say so
  582.         $self->log_( 1, "Ignoring message $msg" );
  583.     }
  584.  
  585.     # After we are done with the folder, we issue an EXPUNGE command
  586.     # if we were told to do so.
  587.  
  588.     if ( $moved_message && $self->config_( 'expunge' ) ) {
  589.         $self->say__( $folder, "EXPUNGE" );
  590.         $self->get_response__( $folder );
  591.     }
  592. }
  593.  
  594.  
  595.  
  596. # ----------------------------------------------------------------------------
  597. #
  598. # classify_message
  599. #
  600. #   This function takes a message UID and then tries to classify the corresponding
  601. #   message to a POPFile bucket. It delegates all the house-keeping that keeps
  602. #   the POPFile statistics up to date to helper functions, but the house-keeping
  603. #   is done. The caller need not worry about this.
  604. #
  605. # Arguments:
  606. #
  607. #   $msg:    UID of the message (the IMAP folder must be SELECTed)
  608. #   $hash:   The hash of the message as computed by get_hash()
  609. #   $folder: The name of the folder on the server in which this message was found
  610. #
  611. # Return value:
  612. #
  613. #   undef on error
  614. #   The name of the destination folder if the message was moved
  615. #   The emtpy string if the message was not moved
  616. #
  617. # ----------------------------------------------------------------------------
  618.  
  619. sub classify_message
  620. {
  621.     my ( $self, $msg, $hash, $folder ) = @_;
  622.  
  623.     my $moved_a_msg = '';
  624.  
  625.     # open a temporary file that the classifier will
  626.     # use to read the message in binary, read-write mode:
  627.     my $pseudo_mailer;
  628.     my $file = $self->get_user_path_( 'imap.tmp' );
  629.     unless ( open $pseudo_mailer, "+>$file" ) {
  630.         $self->log_( 0, "Unable to open temporary file $file. Nothing done to message $msg." );
  631.  
  632.         return;
  633.     }
  634.     binmode $pseudo_mailer;
  635.  
  636.     # We don't retrieve the complete message, but handle
  637.     # it in different parts.
  638.     # Currently these parts are just headers and body.
  639.     # But there is room for improvement here.
  640.     # E.g. we could generate a list of parts by
  641.     # first looking at the parts the message really has.
  642.  
  643.     my @message_parts = qw/HEADER TEXT/;
  644.  
  645.     PART:
  646.     foreach my $part ( @message_parts ) {
  647.  
  648.         my ($ok, @lines ) = $self->fetch_message_part__( $folder, $msg, $part );
  649.  
  650.         unless ( $ok ) {
  651.             $self->log_( 0, "Could not fetch the $part part of message $msg." );
  652.  
  653.             return;
  654.         }
  655.  
  656.         foreach ( @lines ) {
  657.             print $pseudo_mailer "$_";
  658.         }
  659.  
  660.         my ( $class, $slot, $magnet_used );
  661.  
  662.         # If we are dealing with the headers, let the
  663.         # classifier have a non-save go:
  664.  
  665.         if ( $part eq 'HEADER' ) {
  666.             seek $pseudo_mailer, 0, 0;
  667.             ( $class, $slot, $magnet_used ) = $self->{classifier__}->classify_and_modify( $self->{api_session__}, $pseudo_mailer, undef, 1, '', undef, 0, undef );
  668.  
  669.             if ( $magnet_used ) {
  670.                 $self->log_( 0, "Message was classified as $class using a magnet." );
  671.                 print $pseudo_mailer "\nThis message was classified based on a magnet.\nThe body of the message was not retrieved from the server.\n";
  672.             }
  673.             else {
  674.                 next PART;
  675.             }
  676.         }
  677.  
  678.         # We will only get here if the message was magnetized or we
  679.         # are looking at the complete message. Thus we let the classifier have
  680.         # a look and make it save the message to history:
  681.         seek $pseudo_mailer, 0, 0;
  682.  
  683.         ( $class, $slot, $magnet_used ) = $self->{classifier__}->classify_and_modify( $self->{api_session__}, $pseudo_mailer, undef, 0, '', undef, 0, undef );
  684.  
  685.         close $pseudo_mailer;
  686.         unlink $file;
  687.  
  688.         if ( $magnet_used || $part eq 'TEXT' ) {
  689.  
  690.             # Move message:
  691.  
  692.             my $destination = $self->folder_for_bucket__( $class );
  693.             if ( defined $destination ) {
  694.                 if ( $folder ne $destination ) {
  695.                     $self->move_message( $folder, $msg, $destination );
  696.                     $moved_a_msg = $destination;
  697.                 }
  698.             }
  699.             else {
  700.                 $self->log_( 0, "Message cannot be moved because output folder for bucket $class is not defined." );
  701.             }
  702.  
  703.             $self->log_( 0, "Message was classified as $class." );
  704.  
  705.             last PART;
  706.         }
  707.     }
  708.  
  709.     return $moved_a_msg;
  710. }
  711.  
  712.  
  713.  
  714. # ----------------------------------------------------------------------------
  715. #
  716. # reclassify_message
  717. #
  718. #   This function takes a message UID and then tries to reclassify the corresponding
  719. #   message from one POPFile bucket to another POPFile bucket. It delegates all the
  720. #   house-keeping that keeps the POPFile statistics up to date to helper functions,
  721. #   but the house-keeping
  722. #   is done. The caller need not worry about this.
  723. #
  724. # Arguments:
  725. #
  726. #   $folder:     The folder that has received a reclassification request
  727. #   $msg:        UID of the message (the IMAP folder must be SELECTed)
  728. #   $old_bucket: The previous classification of the message
  729. #   $hash:       The hash of the message as computed by get_hash()
  730. #
  731. # Return value:
  732. #
  733. #   undef on error
  734. #   true if things went allright
  735. #
  736. # ----------------------------------------------------------------------------
  737.  
  738. sub reclassify_message
  739. {
  740.     my ( $self, $folder, $msg, $old_bucket, $hash ) = @_;
  741.  
  742.     my $new_bucket = $self->{folders__}{$folder}{output};
  743.     my ( $ok, @lines ) = $self->fetch_message_part__( $folder, $msg, '' );
  744.  
  745.     unless ( $ok ) {
  746.         $self->log_( 0, "Could not fetch message $msg!" );
  747.  
  748.         return;
  749.     }
  750.  
  751.     # We have to write the message to a temporary file.
  752.     # I simply use "imap.tmp" as the file name here.
  753.  
  754.     my $file = $self->get_user_path_( 'imap.tmp' );
  755.     unless ( open TMP, ">$file" ) {
  756.         $self->log_( 0, "Cannot open temp file $file" );
  757.  
  758.         return;
  759.     };
  760.  
  761.     foreach ( @lines ) {
  762.         print TMP "$_\n";
  763.     }
  764.     close TMP;
  765.  
  766.     my $slot = $self->{history__}->get_slot_from_hash( $hash );
  767.  
  768.     $self->{classifier__}->add_message_to_bucket( $self->{api_session__}, $new_bucket, $file );
  769.  
  770.     $self->{classifier__}->reclassified( $self->{api_session__}, $old_bucket, $new_bucket, 0 );
  771.     $self->{history__}->change_slot_classification( $slot, $new_bucket, $self->{api_session__}, 0);
  772.  
  773.     $self->log_( 0, "Reclassified the message with UID $msg from bucket $old_bucket to bucket $new_bucket." );
  774.  
  775.     unlink $file;
  776. }
  777.  
  778.  
  779. # ----------------------------------------------------------------------------
  780. #
  781. # folder_uid_status__
  782. #
  783. #   This function checks the UID status of a given folder on the server.
  784. #   To this end, we look at $self->{last_response} and look for an untagged
  785. #   OK response containing UIDVALIDITY information.
  786. #   Such a response must be send be the server in response to the SELECT
  787. #   command. Thus, this function must only be called after SELECTing a folder.
  788. #
  789. # arguments:
  790. #
  791. #   $folder:        The name of the folder to be inspected.
  792. #
  793. # return value:
  794. #   undef on error (changed uidvalidity)
  795. #   true otherwise
  796. # ----------------------------------------------------------------------------
  797.  
  798. sub folder_uid_status__
  799. {
  800.     my ( $self, $folder ) = @_;
  801.  
  802.     # Save old UIDVALIDITY value (if we have one)
  803.     my $old_val = $self->uid_validity__( $folder );
  804.  
  805.     # Extract current UIDVALIDITY value from server response
  806.     my @lines = split /$eol/, $self->{folders__}{$folder}{last_response};
  807.     my $uidvalidity;
  808.     foreach ( @lines ) {
  809.         if ( /^\* OK \[UIDVALIDITY (\d+)\]/ ) {
  810.             $uidvalidity = $1;
  811.             last;
  812.         }
  813.     }
  814.  
  815.  
  816.     # if we didn't get the value, we have a problem
  817.     unless ( defined $uidvalidity ) {
  818.         $self->log_( 0, "Could not extract UIDVALIDITY status from server response!" );
  819.         return;
  820.     }
  821.  
  822.     # Check whether the old value is still valid
  823.     if ( defined $old_val ) {
  824.         if ( $uidvalidity != $old_val ) {
  825.             $self->log_( 0, "UIDVALIDITY has changed! Expected $old_val, got $uidvalidity." );
  826.             undef $old_val;
  827.         }
  828.     }
  829.  
  830.     # If we haven't got a valid validity value yet, then this
  831.     # must be a new folder for us.
  832.     # In that case, we do an extra STATUS command to get the current value
  833.     # for UIDNEXT.
  834.     unless ( defined $old_val ) {
  835.  
  836.         $self->say__( $folder, "STATUS \"$folder\" (UIDNEXT)" );
  837.         my $response = $self->get_response__( $folder );
  838.  
  839.         if ( $response == 1 ) {
  840.  
  841.             @lines = split /$eol/, $self->{folders__}{$folder}{last_response};
  842.  
  843.             my $uidnext;
  844.  
  845.             foreach ( @lines ) {
  846.                 my $line = $_;
  847.  
  848.                 # We are only interested in untagged responses to the STATUS command
  849.                 next unless $line =~ /\* STATUS/;
  850.  
  851.                 $line =~ /UIDNEXT (.+?)( |\))/i;
  852.                 $uidnext = $1;
  853.  
  854.                 unless ( defined $uidnext ) {
  855.                     $self->log_( 0, "Could not extract UIDNEXT value from server response!!" );
  856.                     return;
  857.                 }
  858.  
  859.                 $self->uid_next__( $folder, $uidnext );
  860.                 $self->uid_validity__( $folder, $uidvalidity );
  861.                 $self->log_( 1, "Updated folder status (UIDVALIDITY and UIDNEXT) for folder $folder." );
  862.             }
  863.         }
  864.         else {
  865.             $self->log_( 0, "Could not STATUS folder $folder!!" );
  866.             return;
  867.         }
  868.     }
  869.     return 1;
  870. }
  871.  
  872.  
  873.  
  874.  
  875. # ----------------------------------------------------------------------------
  876. #
  877. # connect
  878. #
  879. #   Get host and port from the configuration information and
  880. #   connect.
  881. #   Return the socket on sucess or undef on failure
  882. #
  883. # ----------------------------------------------------------------------------
  884.  
  885. sub connect
  886. {
  887.     my ( $self, $hostname, $port ) = @_;
  888.  
  889.     $self->log_( 1, "Connecting to $hostname:$port" );
  890.  
  891.     if ( $hostname ne '' && $port ne '' ) {
  892.  
  893.         my $response = '';
  894.  
  895.         my $imap;
  896.  
  897.         if ( $self->config_( 'use_ssl' ) ) {
  898.             require IO::Socket::SSL;
  899.             $imap = IO::Socket::SSL->new (
  900.                                 Proto    => "tcp",
  901.                                 PeerAddr => $hostname,
  902.                                 PeerPort => $port,
  903.                                 Timeout  => $self->global_config_( 'timeout' )
  904.                                           );
  905.         }
  906.         else {
  907.             $imap = IO::Socket::INET->new(
  908.                                 Proto    => "tcp",
  909.                                 PeerAddr => $hostname,
  910.                                 PeerPort => $port,
  911.                                 Timeout  => $self->global_config_( 'timeout' )
  912.                                          );
  913.         }
  914.  
  915.  
  916.         # Check that the connect succeeded for the remote server
  917.         if ( $imap ) {
  918.             if ( $imap->connected )  {
  919.  
  920.                 # Set binmode on the socket so that no translation of CRLF
  921.                 # occurs
  922.  
  923.                 if ( $self->config_( 'use_ssl' ) == 0 ) {
  924.                     binmode( $imap );
  925.                 }
  926.  
  927.                 # Wait for a response from the remote server and if
  928.                 # there isn't one then give up trying to connect
  929.  
  930.                 my $selector = new IO::Select( $imap );
  931.                 unless ( () = $selector->can_read( $self->global_config_( 'timeout' ) ) ) {
  932.                     $self->log_( 0, "Connection timed out for $hostname:$port" );
  933.                     return;
  934.                 }
  935.  
  936.                 $self->log_( 0, "Connected to $hostname:$port timeout " . $self->global_config_( 'timeout' ) );
  937.  
  938.                 # Read the response from the real server
  939.                 my $buf = $self->slurp_( $imap );
  940.                 $self->log_( 1, ">> $buf" );
  941.                 return $imap;
  942.             }
  943.         }
  944.     }
  945.     else {
  946.         $self->log_( 0, "Invalid port or hostname. Will not connect to server." );
  947.         return;
  948.     }
  949. }
  950.  
  951.  
  952.  
  953.  
  954.  
  955.  
  956. # ----------------------------------------------------------------------------
  957. #
  958. # login
  959. #
  960. #   log in to the server we are currently connected to.
  961. #
  962. # Arguments:
  963. #   $imap: a valid socket object or the name of a folder.
  964. #
  965. # Return values:
  966. #   0 on failure
  967. #   1 on success
  968. # ----------------------------------------------------------------------------
  969.  
  970. sub login
  971. {
  972.     my ( $self, $imap ) = @_;
  973.     my ( $login, $pass ) = ( $self->config_( 'login' ), $self->config_( 'password' ) );
  974.  
  975.     $self->log_( 1, "Logging in" );
  976.  
  977.     $self->say__( $imap, "LOGIN \"$login\" \"$pass\"" );
  978.  
  979.     if ( $self->get_response__( $imap ) == 1 ) {
  980.         return 1;
  981.     }
  982.     else {
  983.         return 0;
  984.     }
  985. }
  986.  
  987.  
  988. # ----------------------------------------------------------------------------
  989. #
  990. # logout
  991. #
  992. #   log out of the the server we are currently connected to.
  993. #
  994. # Arguments:
  995. #   $imap_or_folder: a valid socket object or the name of a folder
  996. #
  997. # Return values:
  998. #   0 on failure
  999. #   1 on success
  1000. # ----------------------------------------------------------------------------
  1001.  
  1002. sub logout
  1003. {
  1004.     my ( $self, $imap_or_folder ) = @_;
  1005.  
  1006.     $self->log_( 1, "Logging out" );
  1007.  
  1008.     $self->say__( $imap_or_folder, "LOGOUT" );
  1009.  
  1010.     if ( $self->get_response__( $imap_or_folder ) == 1 ) {
  1011.         return 1;
  1012.     }
  1013.     else {
  1014.         return 0;
  1015.     }
  1016. }
  1017.  
  1018. # ----------------------------------------------------------------------------
  1019. #
  1020. # raw_say
  1021. #
  1022. #   The worker function for say__. You should normally not need to call this
  1023. #   function directly.
  1024. #
  1025. # Arguments:
  1026. #
  1027. #   $imap:      A valid socket object
  1028. #   $tag:       A numeric value that will be used to tag the commmand
  1029. #   $command:   What you want to say to the server
  1030. #
  1031. # Return value:
  1032. #   undef on error. True on success.
  1033. #
  1034. # ----------------------------------------------------------------------------
  1035.  
  1036. sub raw_say
  1037. {
  1038.     my ( $self, $imap, $tag, $command ) = @_;
  1039.  
  1040.     my $cmdstr = sprintf "A%05d %s%s", $tag, $command, $eol;
  1041.  
  1042.     # Talk to the server
  1043.     unless( print $imap $cmdstr ) {
  1044.         $imap->shutdown( 2 );
  1045.         return;
  1046.     }
  1047.  
  1048.     # Log command
  1049.     # Obfuscate login and password for logins:
  1050.     $cmdstr =~ s/^(A\d+) LOGIN ".+?" ".+"(.+)/$1 LOGIN "xxxxx" "xxxxx"$2/;
  1051.     $self->log_( 1, "<< $cmdstr" );
  1052.  
  1053.     return 1;
  1054. }
  1055.  
  1056.  
  1057.  
  1058. # ----------------------------------------------------------------------------
  1059. #
  1060. # say__
  1061. #
  1062. #   Issue a command to the server we are connected to.
  1063. #
  1064. # Arguments:
  1065. #
  1066. #   $imap_or_folder:
  1067. #       This can be either a valid socket object or the name of a
  1068. #       folder in the $self->{folders__} hash
  1069. #   $command:
  1070. #       What you want to say to the server without the tag, though.
  1071. #
  1072. # Return value:
  1073. #   None. Will die on error, though.
  1074. #
  1075. # ----------------------------------------------------------------------------
  1076.  
  1077. sub say__
  1078. {
  1079.     my ( $self, $imap_or_folder, $command ) = @_;
  1080.  
  1081.     # Did we get a socket object?
  1082.     if ( ref( $imap_or_folder ) eq 'IO::Socket::INET' || ref( $imap_or_folder ) eq 'IO::Socket::SSL' ) {
  1083.  
  1084.         $self->{last_command__} = $command;
  1085.  
  1086.         unless ( $self->raw_say ( $imap_or_folder, $self->{tag__}, $command ) ) {
  1087.             die( "The connection to the IMAP server was lost. Could not talk to the server." );
  1088.         }
  1089.     }
  1090.     # or a folder?
  1091.     else {
  1092.  
  1093.         $self->{folders__}{$imap_or_folder}{last_command} = $command;
  1094.  
  1095.         # Is there a socket connection in the folders hash?
  1096.  
  1097.         unless ( exists $self->{folders__}{$imap_or_folder}{imap} ) {
  1098.             # No! commit suicide.
  1099.             $self->log_( 0, "Got a folder ($imap_or_folder) with no attached socket in say!" );
  1100.             die( "The connection to the IMAP server was lost. Could not talk to the server." );
  1101.         }
  1102.  
  1103.         unless ( $self->raw_say( $self->{folders__}{$imap_or_folder}{imap},
  1104.                                  $self->{folders__}{$imap_or_folder}{tag},
  1105.                                  $command ) ) {
  1106.             # If we failed to talk to the server, delete socket object, and die.
  1107.             delete $self->{folders__}{$imap_or_folder}{imap};
  1108.             die( "The connection to the IMAP server was lost. Could not talk to the server (folder $imap_or_folder)." );
  1109.         }
  1110.     }
  1111. }
  1112.  
  1113.  
  1114. # ----------------------------------------------------------------------------
  1115. #
  1116. # raw_get_response
  1117. #
  1118. #   Get a response from our server. You should normally not need to call this function
  1119. #   directly. Use get_response__ instead.
  1120. #
  1121. # Arguments:
  1122. #
  1123. #   $imap:         A valid socket object
  1124. #   $last_command: The command we are issued before.
  1125. #   $tag_ref:      A reference to a scalar that will receive tag value that can be
  1126. #                  used to tag the next command
  1127. #   $response_ref: A reference to a scalar that will receive the servers response.
  1128. #
  1129. # Return value:
  1130. #   undef   lost connection
  1131. #   1       Server answered OK
  1132. #   0       Server answered NO
  1133. #   -1      Server answered BAD
  1134. #   -2      Server gave unexpected tagged answer
  1135. #   -3      Server didn't say anything, but the connection is still valid (I guess this cannot happen)
  1136. #
  1137. # ----------------------------------------------------------------------------
  1138.  
  1139. sub raw_get_response
  1140. {
  1141.     my ( $self, $imap, $last_command, $tag_ref, $response_ref ) = @_;
  1142.  
  1143.     # What is the actual tag we have to look for?
  1144.     my $actual_tag = sprintf "A%05d", $$tag_ref;
  1145.  
  1146.     my $response = '';
  1147.     my $count_octets = 0;
  1148.     my $octet_count = 0;
  1149.  
  1150.     # Slurp until we find a reason to quit
  1151.     while ( my $buf = $self->slurp_( $imap ) ) {
  1152.  
  1153.         # Check for lost connections:
  1154.         if ( $response eq '' && ! defined $buf ) {
  1155.             $imap->shutdown( 2 );
  1156.             return;
  1157.         }
  1158.  
  1159.         # If this is the first line of the response and
  1160.         # if we find an octet count in curlies before the
  1161.         # newline, then we will rely on the octet count
  1162.  
  1163.         if ( $response eq '' && $buf =~ m/{(\d+)}$eol/ ) {
  1164.  
  1165.             # Add the length of the first line to the
  1166.             # octet count provided by the server
  1167.  
  1168.             $count_octets = $1 + length( $buf );
  1169.         }
  1170.  
  1171.         $response .= $buf;
  1172.  
  1173.         if ( $count_octets ) {
  1174.             $octet_count += length $buf;
  1175.  
  1176.             # There doesn't seem to be a requirement for the message to end with
  1177.             # a newline. So we cannot go for equality
  1178.  
  1179.             if ( $octet_count >= $count_octets ) {
  1180.                 $count_octets = 0;
  1181.             }
  1182.             $self->log_( 2, ">> $buf" );
  1183.         }
  1184.  
  1185.         # If we aren't counting octets (anymore), we look out for tag
  1186.         # followed by BAD, NO, or OK and we also keep an eye open
  1187.         # for untagged responses that the server might send us unsolicited
  1188.         if ( $count_octets == 0 ) {
  1189.             if ( $buf =~ /^$actual_tag (OK|BAD|NO)/ ) {
  1190.  
  1191.                 if ( $1 ne 'OK' ) {
  1192.                     $self->log_( 0, ">> $buf" );
  1193.                 }
  1194.                 else {
  1195.                     $self->log_( 1, ">> $buf" );
  1196.                 }
  1197.  
  1198.                 last;
  1199.             }
  1200.  
  1201.             # Here we look for untagged responses and decide whether they are
  1202.             # solicited or not based on the last command we gave the server.
  1203.  
  1204.             if ( $buf =~ /^\* (.+)/ ) {
  1205.                 my $untagged_response = $1;
  1206.  
  1207.                 $self->log_( 1, ">> $buf" );
  1208.  
  1209.                 # This should never happen, but under very rare circumstances,
  1210.                 # we might get a change of the UIDVALIDITY value while we
  1211.                 # are connected
  1212.                 if ( $untagged_response =~ /UIDVALIDITY/
  1213.                         && $last_command !~ /^SELECT/ ) {
  1214.                     $self->log_( 0, "Got unsolicited UIDVALIDITY response from server while reading response for $last_command." );
  1215.                 }
  1216.  
  1217.                 # This could happen, but will be caught by the eval in service().
  1218.                 # Nevertheless, we look out for unsolicited bye-byes here.
  1219.                 if ( $untagged_response =~ /^BYE/
  1220.                         && $last_command !~ /^LOGOUT/ ) {
  1221.                     $self->log_( 0, "Got unsolicited BYE response from server while reading response for $last_command." );
  1222.                 }
  1223.             }
  1224.         }
  1225.     }
  1226.  
  1227.     # save result away so we can always have a look later on
  1228.     $$response_ref = $response;
  1229.  
  1230.     # Increment tag for the next command/reply sequence:
  1231.     $$tag_ref++;
  1232.  
  1233.     if ( $response ) {
  1234.  
  1235.         # determine our return value
  1236.  
  1237.         # We got 'OK' and the correct tag.
  1238.         if ( $response =~ /^$actual_tag OK/m ) {
  1239.             return 1;
  1240.         }
  1241.         # 'NO' plus correct tag
  1242.         elsif ( $response =~ /^$actual_tag NO/m ) {
  1243.             return 0;
  1244.         }
  1245.         # 'BAD' and correct tag.
  1246.         elsif ( $response =~ /^$actual_tag BAD/m ) {
  1247.             return -1;
  1248.         }
  1249.         # Someting else, probably a different tag, but who knows?
  1250.         else {
  1251.             $self->log_( 0, "!!! Server said something unexpected !!!" );
  1252.             return -2;
  1253.         }
  1254.     }
  1255.     else {
  1256.         $imap->shutdown( 2 );
  1257.         return;
  1258.     }
  1259. }
  1260.  
  1261.  
  1262.  
  1263. # ----------------------------------------------------------------------------
  1264. #
  1265. # get_response__
  1266. #
  1267. # Use this function to get a response from the server. The response will be stored in
  1268. # $self->{last_response__} if you pass in a socket object or in
  1269. # $self->{folders}{$folder}{last_response} if you pass in a folder name
  1270. #
  1271. # Arguments:
  1272. #   $imap_or_folder:
  1273. #       Either a valid socket object or the name of a folder that is stored in the
  1274. #       folders hash.
  1275. #
  1276. #   Return values:
  1277. #      1: Server said OK to our last command
  1278. #      0: Server said NO to our last command
  1279. #     -1: Server said BAD to our last command
  1280. #     -2: Server said something else or reponded to another command
  1281. #     -3: Server didn't say anything
  1282. #   Will die on lost connections!
  1283. # ----------------------------------------------------------------------------
  1284.  
  1285. sub get_response__
  1286. {
  1287.     my ( $self, $imap_or_folder ) = @_;
  1288.  
  1289.     my $result;
  1290.  
  1291.     # Are we dealing with a socket object?
  1292.     if ( ref( $imap_or_folder ) eq 'IO::Socket::INET' ||  ref( $imap_or_folder ) eq 'IO::Socket::SSL' ) {
  1293.         $result = $self->raw_get_response( $imap_or_folder,
  1294.                                               $self->{last_command__},
  1295.                                               \$self->{tag__},
  1296.                                               \$self->{last_response__} );
  1297.         unless ( defined $result ) {
  1298.             die "The connection to the IMAP server was lost. Could not listen to the server.";
  1299.         }
  1300.     }
  1301.     # Or did we get a folder name?
  1302.     else {
  1303.  
  1304.         # Is there a socket object stored in the folders hash?
  1305.         unless ( exists $self->{folders__}{$imap_or_folder}{imap} ) {
  1306.             $self->log_( 0, "Got a folder with no attached socket in get_response!" );
  1307.             die( "The connection to the IMAP server was lost. Could not listen to the server." );
  1308.         }
  1309.  
  1310.         $result = $self->raw_get_response ( $self->{folders__}{$imap_or_folder}{imap},
  1311.                                                $self->{folders__}{$imap_or_folder}{last_command},
  1312.                                               \$self->{folders__}{$imap_or_folder}{tag},
  1313.                                               \$self->{folders__}{$imap_or_folder}{last_response} );
  1314.  
  1315.         # die if we didn't succeed.
  1316.         unless ( defined $result ) {
  1317.             delete $self->{folders__}{$imap_or_folder}{imap};
  1318.             die "The connection to the IMAP server was lost. Could not listen to the server.";
  1319.         }
  1320.  
  1321.     }
  1322.  
  1323.     # return what raw_get_response gave us.
  1324.     return $result;
  1325. }
  1326.  
  1327.  
  1328.  
  1329. # ----------------------------------------------------------------------------
  1330. #
  1331. # get_mailbox_list
  1332. #
  1333. #   Request a list of mailboxes from the server behind the passed in socket object.
  1334. #   The list is stored away in @{$self->{mailboxes__}} and returned.
  1335. #
  1336. # Arguments:
  1337. #   $imap: contains a valid connection to our IMAP server.
  1338. #
  1339. # Return value:
  1340. #
  1341. #   The list of mailboxes
  1342. # ----------------------------------------------------------------------------
  1343.  
  1344. sub get_mailbox_list
  1345. {
  1346.     my ( $self, $imap ) = @_;
  1347.  
  1348.     $self->log_( 1, "Getting mailbox list" );
  1349.  
  1350.     $self->say__( $imap, "LIST \"\" \"*\"" );
  1351.     my $result = $self->get_response__( $imap );
  1352.     if ( $result != 1 ) {
  1353.         $self->log_( 0, "LIST command failed (return value $result)." );
  1354.     }
  1355.  
  1356.     my @lines = split /$eol/, $self->{last_response__};
  1357.     my @mailboxes;
  1358.  
  1359.     foreach ( @lines ) {
  1360.         next unless /^\*/;
  1361.         s/^\* LIST \(.*\) .+? (.+)$/$1/;
  1362.         s/"(.*?)"/$1/;
  1363.         push @mailboxes, $1;
  1364.     }
  1365.  
  1366.     @{$self->{mailboxes__}} = sort @mailboxes;
  1367.  
  1368.     return @{$self->{mailboxes__}};
  1369. }
  1370.  
  1371.  
  1372.  
  1373.  
  1374.  
  1375.  
  1376. # ----------------------------------------------------------------------------
  1377. #
  1378. # fetch_message_part__
  1379. #
  1380. #   This function will fetch a specified part of a specified message from
  1381. #   the IMAP server and return the message as a list of lines.
  1382. #   It assumes that a folder is already SELECTed
  1383. #
  1384. # arguments:
  1385. #
  1386. #   $folder:    the currently selected folder
  1387. #   $msg:       UID of the message
  1388. #   $part:      The part of the message you want to fetch. Could be 'HEADER' for the
  1389. #               message headers, 'TEXT' for the body (including any attachments), or '' to
  1390. #               fetch the complete message. Other values are also possible, but currently
  1391. #               not used. 'BODYSTRUCTURE' could be interesting.
  1392. #
  1393. # return values:
  1394. #
  1395. #       a boolean value indicating success/fallure and
  1396. #       a list containing the lines of the retrieved message (part).
  1397. #
  1398. # ----------------------------------------------------------------------------
  1399.  
  1400. sub fetch_message_part__
  1401. {
  1402.     my ( $self, $folder, $msg, $part ) = @_;
  1403.  
  1404.     if ( $part ne '' ) {
  1405.         $self->log_( 1, "Fetching $part of message $msg" );
  1406.     }
  1407.     else {
  1408.         $self->log_( 1, "Fetching message $msg" );
  1409.     }
  1410.  
  1411.     if ( $part eq 'TEXT' || $part eq '' ) {
  1412.         my $limit = $self->global_config_( 'message_cutoff' );
  1413.         $self->say__( $folder, "UID FETCH $msg (FLAGS BODY.PEEK[$part]<0.$limit>)" );
  1414.     }
  1415.     else {
  1416.         $self->say__( $folder, "UID FETCH $msg (FLAGS BODY.PEEK[$part])" );
  1417.     }
  1418.  
  1419.     my $result = $self->get_response__( $folder );
  1420.  
  1421.     if ( $part ne '' ) {
  1422.         $self->log_( 1, "Got $part of message # $msg, result: $result." );
  1423.     }
  1424.     else {
  1425.         $self->log_( 1, "Got message # $msg, result: $result." );
  1426.     }
  1427.  
  1428.     if ( $result == 1 ) {
  1429.         my @lines = ();
  1430.  
  1431.         # The first line now MUST start with "* x FETCH" where x is a message
  1432.         # sequence number anything else indicates that something went wrong
  1433.         # or that something changed. E.g. the message we wanted
  1434.         # to fetch is no longer there.
  1435.  
  1436.         if ( $self->{folders__}{$folder}{last_response} =~ m/\^* \d+ FETCH/ ) {
  1437.  
  1438.             # The first line should contain the number of octets the server send us
  1439.  
  1440.             if ( $self->{folders__}{$folder}{last_response} =~ m/(?!$eol){(\d+)}$eol/ ) {
  1441.                 my $num_octets = $1;
  1442.  
  1443.                 # Grab the number of octets reported:
  1444.  
  1445.                 my $pos = index $self->{folders__}{$folder}{last_response}, "{$num_octets}$eol";
  1446.                 $pos += length "{$num_octets}$eol";
  1447.  
  1448.                 my $message = substr $self->{folders__}{$folder}{last_response}, $pos, $num_octets;
  1449.  
  1450.                 # Take the large chunk and chop it into single lines
  1451.  
  1452.                 # We cannot use split here, because this would get rid of
  1453.                 # trailing and leading newlines and thus omit complete lines.
  1454.  
  1455.                 while ( $message =~ m/(.*?$eol)/g ) {
  1456.                     push @lines, $1;
  1457.                 }
  1458.             }
  1459.             # No number of octets: fall back, but issue a warning
  1460.             else {
  1461.                 while ( $self->{folders__}{$folder}{last_response} =~ m/(.*?$eol)/g ) {
  1462.                     push @lines, $1;
  1463.                 }
  1464.  
  1465.                 # discard the first and the two last lines; these are server status responses.
  1466.                 shift @lines;
  1467.                 pop @lines;
  1468.                 pop @lines;
  1469.  
  1470.                 $self->log_( 0, "Could not find octet count in server's response!" );
  1471.             }
  1472.         }
  1473.         else {
  1474.             $self->log_( 0, "Unexpected server response to the FETCH command!" );
  1475.         }
  1476.  
  1477.         return 1, @lines;
  1478.     }
  1479.     else {
  1480.         return 0;
  1481.     }
  1482. }
  1483.  
  1484.  
  1485. # ----------------------------------------------------------------------------
  1486. #
  1487. # move_message
  1488. #
  1489. #   Will try to move a message on the IMAP server.
  1490. #
  1491. # arguments:
  1492. #
  1493. #   $imap:
  1494. #       connection to server
  1495. #   $msg:
  1496. #       The UID of the message
  1497. #   $destination:
  1498. #       The destination folder.
  1499. #
  1500. # ----------------------------------------------------------------------------
  1501.  
  1502. sub move_message
  1503. {
  1504.     my ( $self, $folder, $msg, $destination ) = @_;
  1505.  
  1506.     $self->log_( 1, "Moving message $msg to $destination" );
  1507.  
  1508.     my $ok = 0;
  1509.  
  1510.     if ( $self->{folders__}{$folder}{server} == $self->{folders__}{$destination}{server} ) {
  1511.  
  1512.         # Copy message to destination
  1513.         $self->say__( $folder, "UID COPY $msg \"$destination\"" );
  1514.         my $ok = $self->get_response__( $folder );
  1515.  
  1516.         # If that went well, flag it as deleted
  1517.         if ( $ok == 1 ) {
  1518.             $self->say__( $folder, "UID STORE $msg +FLAGS (\\Deleted)" );
  1519.             $ok = $self->get_response__( $folder );
  1520.         }
  1521.         else {
  1522.             $self->log_( 0, "Could not copy message ($ok)!" );
  1523.         }
  1524.     }
  1525.     else {
  1526.         $self->log_( 0, "We don't yet know how to move messages between servers" );
  1527.     }
  1528.  
  1529.     return ( $ok ? 1 : 0 );
  1530. }
  1531.  
  1532.  
  1533. # ----------------------------------------------------------------------------
  1534. #
  1535. # get_new_message_list
  1536. #
  1537. #   Will search for messages on the IMAP server that are not flagged as deleted
  1538. #   that have a UID greater than or equal to the value stored for the passed in folder.
  1539. #
  1540. # arguments:
  1541. #
  1542. #   $folder:       Name of the folder we are looking at.
  1543. #
  1544. # return value:
  1545. #
  1546. #   A list (possibly empty) of the UIDs of matching messages.
  1547. #
  1548. # ----------------------------------------------------------------------------
  1549.  
  1550. sub get_new_message_list
  1551. {
  1552.     my ( $self, $folder ) = @_;
  1553.  
  1554.     my $uid = $self->uid_next__( $folder );
  1555.  
  1556.     $self->log_( 1, "Getting uids ge $uid" );
  1557.  
  1558.     $self->say__( $folder, "UID SEARCH UID $uid:* UNDELETED" );
  1559.     my $result = $self->get_response__( $folder );
  1560.     if ( $result != 1 ) {
  1561.         $self->log_( 0, "SEARCH command failed (return value: $result)!" );
  1562.     }
  1563.  
  1564.     # The server will respond with an untagged search reply.
  1565.     # This can either be empty ("* SEARCH") or if a
  1566.     # message was found it contains the numbers of the matching
  1567.     # messages, e.g. "* SEARCH 2 5 9".
  1568.     # In the latter case, the regexp below will match and
  1569.     # capture the list of messages in $1
  1570.  
  1571.     my @matching = ();
  1572.  
  1573.     if ( $self->{folders__}{$folder}{last_response} =~ /\* SEARCH (.+)$eol/ ) {
  1574.  
  1575.         @matching = split / /, $1;
  1576.     }
  1577.  
  1578.     my @return_list = ();
  1579.  
  1580.     # Make sure that the UIDs reported by the server are really greater
  1581.     # than or equal to our passed in comparison value
  1582.  
  1583.     foreach my $num ( @matching ) {
  1584.         if ( $num >= $uid ) {
  1585.             push @return_list, $num;
  1586.         }
  1587.     }
  1588.  
  1589.     return ( sort { $a <=> $b } @return_list );
  1590. }
  1591.  
  1592.  
  1593.  
  1594. # ----------------------------------------------------------------------------
  1595. #
  1596. #   (g|s)etters for configuration variables
  1597. #
  1598. #
  1599.  
  1600.  
  1601.  
  1602. # ----------------------------------------------------------------------------
  1603. #
  1604. #   folder_for_bucket__
  1605. #
  1606. #   Pass in a bucket name only to get a corresponding folder name
  1607. #   Pass in a bucket name and a folder name to set the pair
  1608. #
  1609. #---------------------------------------------------------------------------------------------
  1610.  
  1611. sub folder_for_bucket__
  1612. {
  1613.     my ( $self, $bucket, $folder ) = @_;
  1614.  
  1615.     my $all = $self->config_( 'bucket_folder_mappings' );
  1616.     my %mapping = split /$cfg_separator/, $all;
  1617.  
  1618.     # set
  1619.     if ( $folder ) {
  1620.         $mapping{$bucket} = $folder;
  1621.  
  1622.         $all = '';
  1623.         while ( my ( $k, $v ) = each %mapping ) {
  1624.             $all .= "$k$cfg_separator$v$cfg_separator";
  1625.         }
  1626.         $self->config_( 'bucket_folder_mappings', $all );
  1627.     }
  1628.     # get
  1629.     else {
  1630.         if ( exists $mapping{$bucket} ) {
  1631.             return $mapping{$bucket};
  1632.         }
  1633.         else {
  1634.             return;
  1635.         }
  1636.     }
  1637. }
  1638.  
  1639.  
  1640. #---------------------------------------------------------------------------------------------
  1641. #
  1642. #   watched_folders__
  1643. #
  1644. #   Returns a list of watched folders when called with no arguments
  1645. #   Otherwise set the list of watched folders to whatever argument happens to be.
  1646. #
  1647. #---------------------------------------------------------------------------------------------
  1648.  
  1649. sub watched_folders__
  1650. {
  1651.     my ( $self, @folders ) = @_;
  1652.  
  1653.     my $all = $self->config_( 'watched_folders' );
  1654.  
  1655.     # set
  1656.     if ( @folders ) {
  1657.         $all = '';
  1658.         foreach ( @folders ) {
  1659.             $all .= "$_$cfg_separator";
  1660.         }
  1661.         $self->config_( 'watched_folders', $all );
  1662.     }
  1663.     # get
  1664.     else {
  1665.         return split /$cfg_separator/, $all;
  1666.     }
  1667. }
  1668.  
  1669.  
  1670. #---------------------------------------------------------------------------------------------
  1671. #
  1672. #   uid_validity__
  1673. #
  1674. #   Pass in a folder name only to get the stored UIDVALIDITY value for that folder
  1675. #   Pass in folder name and new UIDVALIDITY value to store the value
  1676. #
  1677. #---------------------------------------------------------------------------------------------
  1678.  
  1679. sub uid_validity__
  1680. {
  1681.     my ( $self, $folder, $uidval ) = @_;
  1682.  
  1683.     my $all = $self->config_( 'uidvalidities' );
  1684.     my %hash;
  1685.  
  1686.     if ( defined $all ) {
  1687.         %hash = split /$cfg_separator/, $all;
  1688.     }
  1689.  
  1690.  
  1691.     # set
  1692.     if ( defined $uidval ) {
  1693.         $hash{$folder} = $uidval;
  1694.         $all = '';
  1695.         while ( my ( $key, $value ) = each %hash ) {
  1696.             $all .= "$key$cfg_separator$value$cfg_separator";
  1697.         }
  1698.         $self->config_( 'uidvalidities', $all );
  1699.         $self->log_( 1, "Updated UIDVALIDITY value for folder $folder to $uidval." );
  1700.     }
  1701.     # get
  1702.     else {
  1703.         if ( exists $hash{$folder} ) {
  1704.             return $hash{$folder};
  1705.         }
  1706.         else {
  1707.             return;
  1708.         }
  1709.     }
  1710. }
  1711.  
  1712.  
  1713. #---------------------------------------------------------------------------------------------
  1714. #
  1715. #   uid_next__
  1716. #
  1717. #   Pass in a folder name only to get the stored UIDNEXT value for that folder
  1718. #   Pass in folder name and new UIDNEXT value to store the value
  1719. #
  1720. #---------------------------------------------------------------------------------------------
  1721.  
  1722. sub uid_next__
  1723. {
  1724.     my ( $self, $folder, $uidnext ) = @_;
  1725.  
  1726.  
  1727.     my $all = $self->config_( 'uidnexts' );
  1728.     my %hash;
  1729.  
  1730.     if ( defined $all ) {
  1731.         %hash = split /$cfg_separator/, $all;
  1732.     }
  1733.  
  1734.  
  1735.     # set
  1736.     if ( defined $uidnext ) {
  1737.         $hash{$folder} = $uidnext;
  1738.         $all = '';
  1739.         while ( my ( $key, $value ) = each %hash ) {
  1740.             $all .= "$key$cfg_separator$value$cfg_separator";
  1741.         }
  1742.         $self->config_( 'uidnexts', $all );
  1743.         $self->log_( 1, "Updated UIDNEXT value for folder $folder to $uidnext." );
  1744.     }
  1745.     # get
  1746.     else {
  1747.         if ( exists $hash{$folder} ) {
  1748.             return $hash{$folder};
  1749.         }
  1750.         return;
  1751.     }
  1752. }
  1753.  
  1754.  
  1755.  
  1756. # SETTER
  1757.  
  1758. sub classifier
  1759. {
  1760.     my ( $self, $classifier ) = @_;
  1761.  
  1762.     $self->{classifier__} = $classifier;
  1763. }
  1764.  
  1765.  
  1766. sub history
  1767. {
  1768.     my ( $self, $history ) = @_;
  1769.  
  1770.     $self->{history__} = $history;
  1771. }
  1772.  
  1773.  
  1774. #----------------------------------------------------------------------------
  1775. # get hash
  1776. #
  1777. # Computes a hash of the MID and Date header lines of this message.
  1778. # Note that a folder on the server needs to be selected for this to work.
  1779. #
  1780. # Arguments:
  1781. #
  1782. #   $folder:    Name of the folder we are currently servicing.
  1783. #   $msg:       message UID
  1784. #
  1785. # Return value:
  1786. #   A string containing the hash value or undef on error.
  1787. #
  1788. #----------------------------------------------------------------------------
  1789.  
  1790. sub get_hash
  1791. {
  1792.     my ( $self, $folder, $msg ) = @_;
  1793.  
  1794.     my ( $ok, @lines ) = $self->fetch_message_part__( $folder, $msg, "HEADER.FIELDS (Message-id Date Subject Received)" );
  1795.  
  1796.     if ( $ok ) {
  1797.  
  1798.         my %header;
  1799.         my $last;
  1800.  
  1801.         foreach ( @lines ) {
  1802.  
  1803.             s/[\r\n]//g;
  1804.  
  1805.             last if /^$/;
  1806.  
  1807.             if ( /^([^ \t]+):[ \t]*(.*)$/ ) {
  1808.                 $last = lc $1;
  1809.                 push @{$header{$last}}, $2;
  1810.             }
  1811.             else {
  1812.                 if ( defined $last ) {
  1813.                     ${$header{$last}}[$#{$header{$last}}] .= $_;
  1814.                 }
  1815.             }
  1816.         }
  1817.  
  1818.         my $mid      = ${$header{'message-id'}}[0];
  1819.         my $date     = ${$header{'date'}}[0];
  1820.         my $subject  = ${$header{'subject'}}[0];
  1821.         my $received = ${$header{'received'}}[0];
  1822.  
  1823.         my $hash = $self->{history__}->get_message_hash( $mid, $date, $subject, $received );
  1824.  
  1825.         $self->log_( 1, "Hashed message: $subject." );
  1826.         $self->log_( 1, "Message $msg has hash value $hash" );
  1827.  
  1828.         return $hash;
  1829.     }
  1830.     else {
  1831.         $self->log_( 0, "Could not FETCH the header fields of message $msg!" );
  1832.         return;
  1833.     }
  1834. }
  1835.  
  1836.  
  1837.  
  1838. #----------------------------------------------------------------------------
  1839. #   can_classify__
  1840. #
  1841. #   This function is a decider. It decides whether a message can be
  1842. #   classified if found in one of our watched folders or not.
  1843. #
  1844. # arguments:
  1845. #   $hash: The hash value for this message
  1846. #
  1847. # returns true or false
  1848. #----------------------------------------------------------------------------
  1849.  
  1850. sub can_classify__
  1851. {
  1852.     my ( $self, $hash ) = @_;
  1853.  
  1854.     my $slot = $self->{history__}->get_slot_from_hash( $hash );
  1855.  
  1856.     if ( $slot  ne '' ) {
  1857.         $self->log_( 1, "Message was already classified (slot $slot)." );
  1858.         return 0;
  1859.     }
  1860.     else {
  1861.         $self->log_( 1, "The message is not yet in history." );
  1862.         return 1;
  1863.     }
  1864. }
  1865.  
  1866. #----------------------------------------------------------------------------
  1867. #   can_reclassify__
  1868. #
  1869. # This function is a decider. It decides whether a message can be
  1870. # reclassified if found in one of our output folders or not.
  1871. #
  1872. # arguments:
  1873. #   $hash: The hash value for this message
  1874. #
  1875. # return value:
  1876. #   undef if the message should not be reclassified
  1877. #   the current classification if a reclassification is ok
  1878. #----------------------------------------------------------------------------
  1879.  
  1880. sub can_reclassify__
  1881. {
  1882.     my ( $self, $hash, $new_bucket ) = @_;
  1883.  
  1884.     # We must already know the message
  1885.  
  1886.     my $slot = $self->{history__}->get_slot_from_hash( $hash );
  1887.  
  1888.     if ( $slot ne '' ) {
  1889.  
  1890.         my ( $id, $from, $to, $cc, $subject, $date, $hash, $inserted, $bucket, $reclassified ) =
  1891.                     $self->{history__}->get_slot_fields( $slot );
  1892.  
  1893.         $self->log_( 2, "get_slot_fields returned the following information:" );
  1894.         $self->log_( 2, "id:            $id" );
  1895.         $self->log_( 2, "from:          $from" );
  1896.         $self->log_( 2, "to:            $to" );
  1897.         $self->log_( 2, "cc:            $cc" );
  1898.         $self->log_( 2, "subject:       $subject");
  1899.         $self->log_( 2, "date:          $date" );
  1900.         $self->log_( 2, "hash:          $hash" );
  1901.         $self->log_( 2, "inserted:      $inserted" );
  1902.         $self->log_( 2, "bucket:        $bucket" );
  1903.         $self->log_( 2, "reclassified:  $reclassified" );
  1904.  
  1905.         # We must not reclassify a reclassified message
  1906.         if ( ! $reclassified ) {
  1907.  
  1908.             # new and old bucket must be different
  1909.             if ( $new_bucket ne $bucket ) {
  1910.                 return $bucket;
  1911.             }
  1912.             else {
  1913.                 $self->log_( 1, "Will not reclassify to same bucket ($new_bucket)." );
  1914.             }
  1915.         }
  1916.         else {
  1917.             $self->log_( 1, "The message was already reclassified." );
  1918.         }
  1919.     }
  1920.     else {
  1921.         $self->log_( 1, "Message is unknown and cannot be reclassified." );
  1922.     }
  1923.  
  1924.     return;
  1925. }
  1926.  
  1927.  
  1928.  
  1929.  
  1930.  
  1931. # ----------------------------------------------------------------------------
  1932. #
  1933. # configure_item
  1934. #
  1935. #    $name            Name of this item
  1936. #    $templ           The loaded template that was passed as a parameter
  1937. #                     when registering
  1938. #    $language        Current language
  1939. #
  1940. # ----------------------------------------------------------------------------
  1941.  
  1942. sub configure_item
  1943. {
  1944.     my ( $self, $name, $templ, $language ) = @_;
  1945.  
  1946.     # conection details
  1947.     if ( $name eq 'imap_0_connection_details' ) {
  1948.         $templ->param( 'IMAP_hostname', $self->config_( 'hostname' ) );
  1949.         $templ->param( 'IMAP_port',     $self->config_( 'port' ) );
  1950.         $templ->param( 'IMAP_login',    $self->config_( 'login' ) );
  1951.         $templ->param( 'IMAP_password', $self->config_( 'password' ) );
  1952.     }
  1953.  
  1954.     # Which mailboxes/folders should we be watching?
  1955.     if ( $name eq 'imap_1_watch_folders' ) {
  1956.  
  1957.         # We can only configure this when we have a list of mailboxes available on the server
  1958.         if ( @{$self->{mailboxes__}} < 1 || ( ! $self->watched_folders__() ) ) {
  1959.             $templ->param( IMAP_if_mailboxes => 0 );
  1960.         }
  1961.         else {
  1962.             $templ->param( IMAP_if_mailboxes => 1 );
  1963.  
  1964.             # the following code will fill a loop containing another loop
  1965.             # The outer loop iterates over our watched folders,
  1966.             # the inner loop over all our mailboxes to fill the select form
  1967.  
  1968.             # Data for the outer loop, the inner loops data will be contained
  1969.             # in those data structures:
  1970.  
  1971.             my @loop_watched_folders = ();
  1972.  
  1973.             my $i = 0;
  1974.  
  1975.             # Loop over watched folder slot. One select form per watched folder
  1976.             # will be generated
  1977.             foreach my $folder ( $self->watched_folders__() ) {
  1978.                 $i++;
  1979.                 my %data_watched_folders = ();
  1980.  
  1981.                 # inner loop data
  1982.                 my @loop_mailboxes = ();
  1983.  
  1984.                 # loop over IMAP mailboxes and generate a select element for reach one
  1985.                 foreach my $mailbox ( @{$self->{mailboxes__}} ) {
  1986.  
  1987.                     # Populate inner loop entries:
  1988.                     my %data_mailboxes = ();
  1989.  
  1990.                     $data_mailboxes{IMAP_mailbox} = $mailbox;
  1991.  
  1992.                     # Is it currently selected?
  1993.                     if ( $folder eq $mailbox ) {
  1994.                         $data_mailboxes{IMAP_selected} = 'selected="selected"';
  1995.                     }
  1996.                     else {
  1997.                         $data_mailboxes{IMAP_selected} = '';
  1998.                     }
  1999.  
  2000.                     push @loop_mailboxes, \%data_mailboxes;
  2001.                 }
  2002.  
  2003.                 $data_watched_folders{IMAP_loop_mailboxes} = \@loop_mailboxes;
  2004.                 $data_watched_folders{IMAP_loop_counter} = $i;
  2005.                 $data_watched_folders{IMAP_WatchedFolder_Msg} = $$language{Imap_WatchedFolder};
  2006.  
  2007.                 push @loop_watched_folders, \%data_watched_folders;
  2008.             }
  2009.  
  2010.             $templ->param( IMAP_loop_watched_folders => \@loop_watched_folders );
  2011.         }
  2012.     }
  2013.  
  2014.     # Give me another watched folder.
  2015.     if ( $name eq 'imap_2_watch_more_folders' ) {
  2016.         if ( @{$self->{mailboxes__}} < 1 ) {
  2017.             $templ->param( IMAP_if_mailboxes => 0 );
  2018.         }
  2019.         else {
  2020.             $templ->param( IMAP_if_mailboxes => 1 );
  2021.         }
  2022.     }
  2023.  
  2024.  
  2025.     # Which folder corresponds to which bucket?
  2026.     if ( $name eq 'imap_3_bucket_folders' ) {
  2027.         if ( @{$self->{mailboxes__}} < 1 ) {
  2028.             $templ->param( IMAP_if_mailboxes => 0 );
  2029.         }
  2030.         else {
  2031.             $templ->param( IMAP_if_mailboxes => 1 );
  2032.  
  2033.             my @buckets = $self->{classifier__}->get_all_buckets( $self->{api_session__} );
  2034.  
  2035.             my @outer_loop = ();
  2036.  
  2037.             foreach my $bucket ( @buckets ) {
  2038.                 my %outer_data = ();
  2039.                 my $output = $self->folder_for_bucket__( $bucket );
  2040.  
  2041.                 $outer_data{IMAP_mailbox_defined} = (defined $output) ? 1 : 0;
  2042.                 $outer_data{IMAP_Bucket_Header} = sprintf( $$language{Imap_Bucket2Folder}, $bucket );
  2043.  
  2044.                 my @inner_loop = ();
  2045.                 foreach my $mailbox ( @{$self->{mailboxes__}} ) {
  2046.                     my %inner_data = ();
  2047.  
  2048.                     $inner_data{IMAP_mailbox} = $mailbox;
  2049.  
  2050.                     if ( defined $output && $output eq $mailbox ) {
  2051.                         $inner_data{IMAP_selected} = 'selected="selected"';
  2052.                     }
  2053.                     else {
  2054.                         $inner_data{IMAP_selected} = '';
  2055.                     }
  2056.  
  2057.                     push @inner_loop, \%inner_data;
  2058.                 }
  2059.                 $outer_data{IMAP_loop_mailboxes} = \@inner_loop;
  2060.                 $outer_data{IMAP_bucket} = $bucket;
  2061.                 push @outer_loop, \%outer_data;
  2062.             }
  2063.             $templ->param( IMAP_loop_buckets => \@outer_loop );
  2064.         }
  2065.     }
  2066.  
  2067.  
  2068.  
  2069.     # Read the list of mailboxes from the server. Now!
  2070.     if ( $name eq 'imap_4_update_mailbox_list' ) {
  2071.         if ( $self->config_( 'hostname' ) eq '' ) {
  2072.             $templ->param( IMAP_if_connection_configured => 0 );
  2073.         }
  2074.         else {
  2075.             $templ->param( IMAP_if_connection_configured => 1 );
  2076.         }
  2077.     }
  2078.  
  2079.  
  2080.     # Various options for the IMAP module
  2081.     if ( $name eq 'imap_5_options' ) {
  2082.  
  2083.         # Are we expunging after moving messages?
  2084.         my $checked = $self->config_( 'expunge' ) ? 'checked="checked"' : '';
  2085.         $templ->param( IMAP_expunge_is_checked => $checked );
  2086.  
  2087.         # Update interval in seconds
  2088.         $templ->param( IMAP_interval => $self->config_( 'update_interval' ) );
  2089.     }
  2090. }
  2091.  
  2092.  
  2093.  
  2094. # ----------------------------------------------------------------------------
  2095. #
  2096. # validate_item
  2097. #
  2098. #    $name            The name of the item being configured, was passed in by the call
  2099. #                     to register_configuration_item
  2100. #    $templ           The loaded template
  2101. #    $language        The language currently in use
  2102. #    $form            Hash containing all form items
  2103. #
  2104. # ----------------------------------------------------------------------------
  2105.  
  2106. sub validate_item
  2107. {
  2108.     my ( $self, $name, $templ, $language, $form ) = @_;
  2109.  
  2110.     # connection details
  2111.     if ( $name eq 'imap_0_connection_details' ) {
  2112.         if ( defined $$form{update_imap_0_connection_details} ) {
  2113.             if ( $$form{imap_hostname} ne '' ) {
  2114.                 $templ->param( IMAP_connection_if_hostname_error => 0 );
  2115.                 $self->config_( 'hostname', $$form{imap_hostname} );
  2116.             }
  2117.             else {
  2118.                 $templ->param( IMAP_connection_if_hostname_error => 1 );
  2119.             }
  2120.  
  2121.             if ( $$form{imap_port} >= 1 && $$form{imap_port} < 65536 ) {
  2122.                 $self->config_( 'port', $$form{imap_port} );
  2123.                 $templ->param( IMAP_connection_if_port_error => 0 );
  2124.             }
  2125.             else {
  2126.                 $templ->param( IMAP_connection_if_port_error => 1 );
  2127.             }
  2128.  
  2129.             if ( $$form{imap_login} ne '' ) {
  2130.                 $self->config_( 'login', $$form{imap_login} );
  2131.                 $templ->param( IMAP_connection_if_login_error => 0 );
  2132.             }
  2133.             else {
  2134.                 $templ->param( IMAP_connection_if_login_error => 1 );
  2135.             }
  2136.  
  2137.             if ( $$form{imap_password} ne '' ) {
  2138.                 $self->config_( 'password', $$form{imap_password} );
  2139.                 $templ->param( IMAP_connection_if_password_error => 0 );
  2140.             }
  2141.             else {
  2142.                 $templ->param( IMAP_connection_if_password_error => 1 );
  2143.             }
  2144.         }
  2145.         return;
  2146.     }
  2147.  
  2148.     # watched folders
  2149.     if ( $name eq 'imap_1_watch_folders' ) {
  2150.         if ( defined $$form{update_imap_1_watch_folders} ) {
  2151.  
  2152.             my $i = 1;
  2153.             my %folders;
  2154.             foreach ( $self->watched_folders__() ) {
  2155.                 $folders{ $$form{"imap_folder_$i"} }++;
  2156.                 $i++;
  2157.             }
  2158.  
  2159.             $self->watched_folders__( sort keys %folders );
  2160.             $self->{folder_change_flag__} = 1;
  2161.         }
  2162.         return;
  2163.     }
  2164.  
  2165.     # Add a watched folder
  2166.     if ( $name eq 'imap_2_watch_more_folders' ) {
  2167.         if ( defined $$form{imap_2_watch_more_folders} ) {
  2168.             my @current = $self->watched_folders__();
  2169.             push @current, 'INBOX';
  2170.             $self->watched_folders__( @current );
  2171.         }
  2172.         return;
  2173.     }
  2174.  
  2175.     # map buckets to folders
  2176.     if ( $name eq 'imap_3_bucket_folders' ) {
  2177.         if ( defined $$form{imap_3_bucket_folders} ) {
  2178.  
  2179.             # We have to make sure that there is only one bucket per folder
  2180.             # Multiple buckets cannot map to the same folder because how
  2181.             # could we reliably reclassify on move then?
  2182.  
  2183.             my %bucket2folder;
  2184.             my %folders;
  2185.  
  2186.             foreach my $key ( keys %$form ) {
  2187.                 # match bucket name:
  2188.                 if ( $key =~ /^imap_folder_for_(.+)$/ ) {
  2189.                     my $bucket = $1;
  2190.                     my $folder = $$form{ $key };
  2191.  
  2192.                     $bucket2folder{ $bucket } = $folder;
  2193.  
  2194.                     # pseudo buckets are free to map wherever they like since
  2195.                     # we will never reclassify to them anyway
  2196.                     unless ( $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket ) ) {
  2197.                         $folders{ $folder }++;
  2198.                     }
  2199.                 }
  2200.             }
  2201.  
  2202.             my $bad = 0;
  2203.             while ( my ( $bucket, $folder ) = each %bucket2folder ) {
  2204.  
  2205.                 if ( exists $folders{$folder} && $folders{ $folder } > 1 ) {
  2206.                     $bad = 1;
  2207.                 }
  2208.                 else {
  2209.                     $self->folder_for_bucket__( $bucket, $folder );
  2210.  
  2211.                     $self->{folder_change_flag__} = 1;
  2212.                 }
  2213.             }
  2214.             $templ->param( IMAP_buckets_to_folders_if_error => $bad );
  2215.         }
  2216.         return;
  2217.     }
  2218.  
  2219.     # update the list of mailboxes
  2220.     if ( $name eq 'imap_4_update_mailbox_list' ) {
  2221.         if ( defined $$form{do_imap_4_update_mailbox_list} ) {
  2222.             if ( $self->config_( 'hostname' )
  2223.                 && $self->config_( 'login' )
  2224.                 && $self->config_( 'login' )
  2225.                 && $self->config_( 'port' )
  2226.                 && $self->config_( 'password' ) ) {
  2227.  
  2228.                     my $imap = $self->connect( $self->config_( 'hostname' ), $self->config_( 'port' ) );
  2229.                     if ( defined $imap ) {
  2230.                         if ( $self->login( $imap ) ) {;
  2231.                             $self->get_mailbox_list( $imap );
  2232.                             $self->logout( $imap );
  2233.                             $templ->param( IMAP_update_list_failed => '' );
  2234.                         }
  2235.                         else {
  2236.                             $templ->param( IMAP_update_list_failed => 'Could not login. Verify your login name and password, please.' );
  2237.                             # should be language__{Imap_UpdateError1}
  2238.                         }
  2239.                     }
  2240.                     else {
  2241.                         $templ->param( IMAP_update_list_failed => 'Failed to connect to server. Please check the host name and port and make sure you are online.' );
  2242.                         # should be language__{Imap_UpdateError2}
  2243.                     }
  2244.             }
  2245.             else {
  2246.                 $templ->param( IMAP_update_list_failed => 'Please configure the connection details first.' );
  2247.                 # should be language__{Imap_UpdateError3}
  2248.             }
  2249.         }
  2250.         return;
  2251.     }
  2252.  
  2253.  
  2254.     # various options
  2255.     if ( $name eq 'imap_5_options' ) {
  2256.  
  2257.         if ( defined $$form{update_imap_5_options} ) {
  2258.  
  2259.             # expunge or not?
  2260.             if ( defined $$form{imap_options_expunge} ) {
  2261.                 $self->config_( 'expunge', 1 );
  2262.             }
  2263.             else {
  2264.                 $self->config_( 'expunge', 0 );
  2265.             }
  2266.  
  2267.             # update interval
  2268.             my $form_interval = $$form{imap_options_update_interval};
  2269.             if ( defined $form_interval ) {
  2270.                 if ( $form_interval > 10 && $form_interval < 60*60 ) {
  2271.                     $self->config_( 'update_interval', $form_interval );
  2272.                     $templ->param( IMAP_if_interval_error => 0 );
  2273.                 }
  2274.                 else {
  2275.                     $templ->param( IMAP_if_interval_error => 1 );
  2276.                 }
  2277.             }
  2278.             else {
  2279.                 $templ->param( IMAP_if_interval_error => 1 );
  2280.             }
  2281.         }
  2282.         return;
  2283.     }
  2284.  
  2285.  
  2286.     $self->SUPER::validate_item( $name, $templ, $language, $form );
  2287. }
  2288.  
  2289.  
  2290. sub train_on_archive__
  2291. {
  2292.     my ( $self ) = @_;
  2293.  
  2294.     $self->log_( 0, "Training on existing archive." );
  2295.  
  2296.     # Reset the folders hash and build it again.
  2297.  
  2298.     %{$self->{folders__}} = ();
  2299.     $self->build_folder_list__();
  2300.  
  2301.     # eliminate all watched folders
  2302.     foreach my $folder ( keys %{$self->{folders__}} ) {
  2303.         if ( exists $self->{folders__}{$folder}{watched} ) {
  2304.             delete $self->{folders__}{$folder};
  2305.         }
  2306.     }
  2307.  
  2308.     # Connect to server
  2309.     $self->connect_folders__();
  2310.  
  2311.     foreach my $folder ( keys %{$self->{folders__}} ) {
  2312.  
  2313.         # Set uidnext value to 1. We will train on all messages.
  2314.         $self->uid_next__( $folder, 1 );
  2315.         my @uids = $self->get_new_message_list( $folder );
  2316.         my $bucket = $self->{folders__}{$folder}{output};
  2317.  
  2318.         # Skip pseudobuckets and the INBOX
  2319.         next if $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket );
  2320.         next if $folder eq 'INBOX';
  2321.  
  2322.         $self->log_( 0, "Training on messages in folder $folder to bucket $bucket." );
  2323.  
  2324.         foreach my $msg ( @uids ) {
  2325.  
  2326.             my ( $ok, @lines ) = $self->fetch_message_part__( $folder, $msg, '' );
  2327.  
  2328.             $self->uid_next__( $folder, $msg );
  2329.  
  2330.             unless ( $ok ) {
  2331.                 $self->log_( 0, "Could not fetch message $msg!" );
  2332.                 next;
  2333.             }
  2334.  
  2335.             my $file = $self->get_user_path_( 'imap.tmp' );
  2336.             unless ( open TMP, ">$file" ) {
  2337.                 $self->log_( 0, "Cannot open temp file $file" );
  2338.                 next;
  2339.             };
  2340.  
  2341.             foreach ( @lines ) {
  2342.                 print TMP "$_\n";
  2343.             }
  2344.             close TMP;
  2345.  
  2346.             $self->{classifier__}->add_message_to_bucket( $self->{api_session__}, $bucket, $file );
  2347.  
  2348.             $self->log_( 0, "Training on the message with UID $msg to bucket $bucket." );
  2349.  
  2350.             unlink $file;
  2351.  
  2352.         }
  2353.     }
  2354.     # Again, reset folders__ hash.
  2355.     %{$self->{folders__}} = ();
  2356.  
  2357.     # And disable training mode so we won't do this again the next time service is called.
  2358.     $self->config_( 'training_mode', 0 );
  2359. }
  2360.  
  2361.  
  2362. 1;
  2363.  
  2364.